﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.ComponentModel;
using System.Net;

namespace YouTubeStormer
{
    class Client
    {
        public delegate void CommentPerformedHandler(Client sender, bool successful, bool commentPending = false);
        public event CommentPerformedHandler CommentPerformed;

        public delegate void VotePerformedHandler(Client sender, bool successful);
        public event VotePerformedHandler VotePerformed;

        public delegate void LoginPerformedHandler(Client sender, bool successful);
        public event LoginPerformedHandler LoginPerformed;

        public delegate void DecaptchaPerformedHandler(Client sender, bool successful, bool commentCaptcha);
        public event DecaptchaPerformedHandler DecaptchaPerformed;

        public delegate void ConnectionErrorHandler(Client sender);
        public event ConnectionErrorHandler ConnectionError;

        public delegate void CaptchaErrorHandler(Client sender);
        public event CaptchaErrorHandler CaptchaError;

        public Configuration.AccountInfo Account { get; set; }
        Communications communications;
        Captcha captchaService;
        bool loggedIn = false;
        int lastCaptchaID = 0;

        public DateTime LastCommentTimeStamp { get; set; }
        public DateTime LastVoteTimeStamp { get; set; }
        public string LastVideoID { get; set; }
        public bool IsBusy { get; set; }
        bool alreadyPerformedCaptcha = false;

        BackgroundWorker workerCommenting;
        BackgroundWorker workerVoting;
        WorkerInfoTransport workerInfoTranport = new WorkerInfoTransport();

        public class WorkerInfoTransport
        {
            public string CommentText{get; set;}
            public Configuration.CommentInfo Comment { get; set; }
        }

        public Client(Configuration.AccountInfo account, Captcha captchaService)
        {

            this.captchaService = captchaService;

            Account = account;
            communications = new Communications(Account.Proxy);

            workerCommenting = new BackgroundWorker();
            workerCommenting.DoWork += workerCommenting_DoWork;
            workerCommenting.RunWorkerCompleted += workerCommenting_RunWorkerCompleted;

            workerVoting = new BackgroundWorker();
            workerVoting.DoWork += workerVoting_DoWork;
            workerVoting.RunWorkerCompleted += workerVoting_RunWorkerCompleted;
        }

        void workerCommenting_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            LastCommentTimeStamp = DateTime.Now;
            CheckIBusy();
        }

        void workerVoting_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            LastVoteTimeStamp = DateTime.Now;
            CheckIBusy();
        }

        void CheckIBusy()
        {
            IsBusy = (workerCommenting.IsBusy || workerVoting.IsBusy);
        }

        void workerVoting_DoWork(object sender, DoWorkEventArgs e)
        {
            if (!PerformLoginCheck()) return;

            while (communications.PerformGet(Urls.YoutubeWatchUrl + LastVideoID, allowRedirects: true) == "") { }

            string getUrl = Urls.YoutubeBaseUrl + "comment_voting?" + 
                "a=" + "1" +
                "&id=" + workerInfoTranport.Comment.ID +
                "&video_id=" + LastVideoID +
                "&old_vote=" + workerInfoTranport.Comment.Score +
                "&tag=" + workerInfoTranport.Comment.Tag;

            string ResponseData = communications.PerformGet(
                getUrl,
                referer: Urls.YoutubeWatchUrl + LastVideoID,
                host: Urls.YoutubeBareHostUrl,
                accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");

            CheckConnectionError(ResponseData);

            if (ResponseData.Contains("comment-score"))
            {
                Properties.Settings.Default.VoteLog.Add(Account.Username + ":" + workerInfoTranport.Comment.ID);
                Properties.Settings.Default.Save();
                VotePerformed(this, true);
            }
            else
                VotePerformed(this, false);

        }

        void workerCommenting_DoWork(object sender, DoWorkEventArgs e)
        {
            if (!PerformLoginCheck()) 
                return;

            string ResponseData = CheckDasCatpcha();

            CheckConnectionError(ResponseData);

            string sessionToken = IsolateSessionTokenFromBlock(ResponseData);
            var commentBlock = SendComment(sessionToken);

            var comment = Configuration.ParseComment(commentBlock);
            if (comment != null)
            {
                Properties.Settings.Default.VoteLog.Add(Account.Username + ":" + comment.ID);
                Properties.Settings.Default.Save();
                CommentPerformed(this, true, commentPending: comment.Pending);
            }
            else
                CommentPerformed(this, false);
        }

        string CheckDasCatpcha()
        {
            var document = communications.PerformGet(Urls.GetVideoUrl(LastVideoID), allowRedirects: true);
            string sessionToken = IsolateSessionTokenFromBlock(document);

            if (document.Contains(Urls.CaptchaTriggerString))
            {
                if (!CheckCaptchaReporting()) DecaptchaPerformed(this, false, false);
                string captchaID = Regex.Split(document, Urls.CaptchaTriggerString)[1].Split('"')[0];
                WebResponse streamToClose;
                var image = communications.StreamToByteArray(communications.PerformGetWithStream(Urls.YoutubeCaptchaImageUrl + captchaID, out streamToClose));
                streamToClose.Close();

                if (image == null)
                {
                    ConnectionError(this);
                    return CheckDasCatpcha();
                }

                var captchaText = captchaService.SolveCatpcha(image, out lastCaptchaID);

                if (captchaText == "")
                {
                    lastCaptchaID = 0;
                    CaptchaError(this);
                    return CheckDasCatpcha();
                }

                var postData = new Communications.PostData();
                postData.AddValue("challenge_enc", captchaID);
                postData.AddValue("next", "/index");
                postData.AddValue("response", captchaText);
                postData.AddValue("action_verify", "1");
                postData.AddValue("submit", "Submit");
                postData.AddValue("session_token", sessionToken);

                communications.PerformPost(Urls.YoutubeCaptchaDasUrl, postData, referer: Urls.YoutubeCaptchaDasUrl);
                return CheckDasCatpcha();
            }
            else
            {
                if (alreadyPerformedCaptcha) DecaptchaPerformed(this, true, false);
                alreadyPerformedCaptcha = false;
                return document;
            }
        }

        string IsolateSessionTokenFromBlock(string block)
        {
            string tokenSplitter = "session_token\" value=\"";
            string oddTokenSplitter = "XSRF_TOKEN': '";
            if (block.Contains(tokenSplitter))
                return Regex.Split(Regex.Split(block, tokenSplitter)[1], "\"/>")[0];
            else if (block.Contains(oddTokenSplitter))
                return Regex.Split(Regex.Split(block, oddTokenSplitter)[1], "',")[0];

            return "";
        }

        string SendComment(string sessionToken, List<KeyValuePair<string, string>> extraData = null)
        {
            var postData = new Communications.PostData();
            postData.AddValue("form_id", "");
            postData.AddValue("reply_parent_id", "");
            postData.AddValue("video_id", LastVideoID);
            postData.AddValue("return_ajax", "true");
            postData.AddValue("session_token", sessionToken);
            postData.AddValue("comment", workerInfoTranport.CommentText);

            if (extraData != null)
                foreach (KeyValuePair<string, string> extra in extraData)
                {
                    postData.AddValue(extra.Key, extra.Value);
                }

            string ResponseData = communications.PerformPost(Urls.YoutubeCommentUrl + "len=" + workerInfoTranport.CommentText.Length + "&wc=5", postData, referer: Urls.YoutubeWatchUrl + LastVideoID);

            if (!CheckConnectionError(ResponseData))
            {
                return CheckDasCatpcha();
            }
            
            if (ResponseData.Contains("INLINE_CAPTCHA"))
            {
                if (!CheckCaptchaReporting()) DecaptchaPerformed(this, false, true);

                string captchaPage = communications.PerformPost(Urls.YoutubeCaptchaFetchUrl, new Communications.PostData());
                var challengeID = Regex.Split(captchaPage, "challenge\" value=\"")[1].Split('"')[0];
                WebResponse streamToClose;
                var image = communications.StreamToByteArray(communications.PerformGetWithStream(Urls.YoutubeCaptchaImageUrl + challengeID, out streamToClose));
                streamToClose.Close();

                if (image == null)
                {
                    ConnectionError(this);
                    return CheckDasCatpcha();
                }

                ResponseData = captchaService.SolveCatpcha(image, out lastCaptchaID);

                if (ResponseData == "")
                {
                    CaptchaError(this);
                    return SendComment(sessionToken, extraData);
                }

                var captchaPostData = new List<KeyValuePair<string, string>>();
                captchaPostData.Add(new KeyValuePair<string, string>("response", ResponseData));
                captchaPostData.Add(new KeyValuePair<string, string>("challenge", challengeID));
                return SendComment(sessionToken, captchaPostData);
            }
            else
            {
                if (alreadyPerformedCaptcha) DecaptchaPerformed(this, true, true);
                alreadyPerformedCaptcha = false;
                return ResponseData;
            }
           
        }

        bool CheckCaptchaReporting()
        {
            if (alreadyPerformedCaptcha)
            {
                captchaService.ReportCaptcha(lastCaptchaID);
            }
            bool repeat = alreadyPerformedCaptcha;
            alreadyPerformedCaptcha = true;
            return !repeat;
        }

        bool PerformLoginCheck()
        {
            IsBusy = true;
            if (!loggedIn)
            {
                if (PerformLogin())
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }
            else return true;
        }

        bool PerformLogin()
        {
            try
            {
                var ResponseData = communications.PerformGet(Urls.YoutubePreAuthenticationUrl, allowRedirects: true);

                CheckConnectionError(ResponseData);

                if (ResponseData == null)
                    return false;

                var GALX = Regex.Match(ResponseData, "name=\"GALX\"[\\s]*value=\"([a-zA-Z0-9-_\\.]*)\"").Groups[1].ToString();
                var dsh = Regex.Match(ResponseData, "id=\"dsh\"[\\s]*value=\"([a-zA-Z0-9-_\\.]*)\"").Groups[1].ToString();
                var uilel = Regex.Match(ResponseData, "id=\"uilel\"[\\s]*value=\"([a-zA-Z0-9-_\\.]*)\"").Groups[1].ToString();

                var postData = new Communications.PostData();
                postData.AddValue("ltmpl", "sso");
                postData.AddValue("continue", Urls.YoutubeAuthContinueUrl);
                postData.AddValue("service", "youtube");
                postData.AddValue("uilel", uilel);
                postData.AddValue("dsh", dsh);
                postData.AddValue("hl", "en_US");
                postData.AddValue("GALX", GALX);
                postData.AddValue("Email", Account.Username);
                postData.AddValue("Passwd", Account.Password);
                postData.AddValue("PersistentCookie", "yes");
                postData.AddValue("rmShown", "1");
                postData.AddValue("signIn", "Sign in");
                postData.AddValue("asts", "");

                ResponseData = communications.PerformPost(Urls.YoutubeAuthenticationUrl, postData, Urls.YoutubePreAuthenticationUrl);
                ResponseData = communications.PerformGet(Urls.YoutubeCheckCookieUrl, referer: Urls.YoutubeAuthenticationUrl);
                var checkResponse = communications.PerformGet(Urls.YoutubeLoginAuthorizationUrl + Regex.Split(Regex.Split(ResponseData, "auth=")[1], "&#39;")[0]);

                CheckConnectionError(ResponseData);

                if (ResponseData.Contains("<title>Redirecting</title>"))
                {
                    loggedIn = true;
                    LoginPerformed(this, true);
                    return true;
                }
                else
                {
                    loggedIn = false;
                    LoginPerformed(this, false);
                    return false;
                }
            }
            catch
            {
                loggedIn = false;
                LoginPerformed(this, false);
                return false;
            }
        }

        public void PerformComment(string videoID, string commentText)
        {
            IsBusy = true;
            LastVideoID = videoID;
            workerInfoTranport.CommentText = commentText;
            workerCommenting.RunWorkerAsync();
        }

        public void PerformVote(string videoID, Configuration.CommentInfo comment)
        {
            IsBusy = true;
            LastVideoID = videoID;
            workerInfoTranport.Comment = comment;
            workerVoting.RunWorkerAsync();
        }

        public bool CheckConnectionError(string response)
        {
            if (response == "")
            {
                ConnectionError(this);
                return false;
            }
            return true;
        }

    }
}
